package com.piece.core.log.aspect;

import com.piece.core.framework.constant.ApplicationConstants;
import com.piece.core.framework.enums.StatusType;
import com.piece.core.framework.properties.ApplicationProperties;
import com.piece.core.framework.support.convert.Convert;
import com.piece.core.framework.util.async.AsyncManager;
import com.piece.core.framework.util.collection.ArrayUtil;
import com.piece.core.framework.util.collection.CollectionUtil;
import com.piece.core.framework.util.object.BeanUtil;
import com.piece.core.framework.util.string.StringUtil;
import com.piece.core.log.annotation.AuditDiffDetail;
import com.piece.core.log.client.dto.BeanPostLogDTO;
import com.piece.core.log.client.dto.BeanPostLogInfoDTO;
import com.piece.core.log.client.dto.VisitInfoDTO;
import com.piece.core.log.config.*;
import com.piece.core.log.client.LogFactory;
import com.piece.core.log.enums.AuditLogType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.persistence.Transient;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;

/**
 * 审计日志切面
 */
@Slf4j
@Aspect
@Order(100)
@ConditionalOnClass({HttpServletRequest.class, org.springframework.web.context.request.RequestContextHolder.class})
public class AuditLogAspect implements BeanFactoryAware {

    @Value("${spring.application.name:#{null}}")
    private String applicationName;

    // 用于SpEL表达式解析
    private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
    // 定义解析的模板
    private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext();
    // 参数名发现器
    private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();

    private ApplicationProperties applicationProperties;
    private final LogFactory logFactory;
    private BeanFactory beanFactory;

    public AuditLogAspect(ApplicationProperties applicationProperties, LogFactory logFactory) {
        this.applicationProperties = applicationProperties;
        this.logFactory = logFactory;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Around("@within(auditLog) || @annotation(auditLog)")
    public Object doAround(ProceedingJoinPoint joinPoint, com.piece.core.log.annotation.AuditLog auditLog) throws Throwable {
        Object result = null;
        if (null == auditLog) {
            // 获取类上的注解
            auditLog = joinPoint.getTarget().getClass().getDeclaredAnnotation(com.piece.core.log.annotation.AuditLog.class);
        }
        // 加载上下文数据
        StandardEvaluationContext context = AuditLogContextHolder.getContext(beanFactory);
        AuditLogFunctionRegister.register(context);
        Method method = getMethod(joinPoint);
        Object[] args = joinPoint.getArgs();
        String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
        if (ArrayUtil.isNotEmpty(paramNames)) {
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
        }
        AuditLogContextHolder.putContext(context);

        ServletRequestAttributes attributes = (ServletRequestAttributes) org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        RequestContextHolder.set(request, request.getHeader("user-agent"));

        AuditLogType type = auditLog.operationType();
        try {
            if (null == logFactory) {
                log.warn("Spring Bean [auditLogFactory] is null");
                return joinPoint.proceed();
            }

            String entityId = auditLog.bizId();
            if (StringUtils.isNotEmpty(entityId)) {
                entityId = Convert.toStr(parseExpress(entityId, AuditLogContextHolder.getContext()), null);
                // 如果是新增类别，修改为变更类别
                if (StringUtil.isNotEmpty(entityId) && AuditLogType.INSERT.equals(type)) {
                    type = AuditLogType.UPDATE;
                }
            }

            AuditDiffDetail auditDiffDetail = method.getDeclaredAnnotation(AuditDiffDetail.class);
            if (null != auditDiffDetail) {
                Object source = parseExpress(auditDiffDetail.sourceProxy(), context);
                if (StringUtil.isNotEmpty(auditDiffDetail.fileName()) && auditDiffDetail.sourceProxy().indexOf("@") != -1) {
                    // 提取返回数据的某个属性
                    source = BeanUtil.getFieldValue(source, auditDiffDetail.fileName());
                }
                Object target = parseExpress(auditDiffDetail.targetProxy(), context);
                List<Map> diffRecord = BeanUtil.diffRecord(source, target, auditDiffDetail.fileName(), Arrays.asList(Transient.class));
                AuditLogContextHolder.putVariable(AuditLogContextHolder.CONTEXT_KEY_NAME_DIFF_RECORD, diffRecord);
            }

            result = joinPoint.proceed();
            // 记录日志
            if (Convert.toBool(applicationProperties.getValue(ApplicationConstants.SYSTEM_MODIFY_LOG_ENABLE), false)) {
                handleLog(joinPoint, method, type, auditLog, null);
            }
        } catch (Throwable throwable) {
            // 记录日志
            if (Convert.toBool(applicationProperties.getValue(ApplicationConstants.SYSTEM_MODIFY_LOG_ENABLE), false)) {
                handleLog(joinPoint, method, type, auditLog, throwable);
            }
            // 抛出原方法异常
            throw throwable;
        } finally {
            return result;
        }
    }

    protected void handleLog(ProceedingJoinPoint joinPoint, Method method, AuditLogType type, com.piece.core.log.annotation.AuditLog auditLog, final Throwable throwable) {
        if (Convert.toBool(applicationProperties.getValue(ApplicationConstants.SYSTEM_MODIFY_LOG_ENABLE), false)) {
            if (null != logFactory.getAuditLog()) {
                AsyncManager.getInstance().submit(() -> {
                    saveAudit(type, auditLog, joinPoint, throwable);
                });
            }
        }
    }

    /**
     * 构建审计对象
     */
    private void saveAudit(AuditLogType type, com.piece.core.log.annotation.AuditLog auditLog, ProceedingJoinPoint joinPoint, Throwable throwable) {
        BeanPostLogDTO audit = new BeanPostLogDTO();
        // 记录应用信息
        audit.setApplicationName(applicationName);
        audit.setSource(auditLog.source());
        audit.setTableName(auditLog.table());
        audit.setType(type.getType() + "");
        // 记录方法信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        audit.setClassName(methodSignature.getDeclaringTypeName());
        audit.setMethodName(methodSignature.getName());
        // 记录操作人员
        audit.setClientId(TenantContextHolder.getTenant());
        audit.setOperaId(UserContextHolder.getUserId());
        audit.setOperaName(UserContextHolder.getUserName());
        audit.setOperaTime(new Date());
        // 记录访问来源信息
        VisitInfoDTO visitInfo = RequestContextHolder.get();
        audit.setIp(visitInfo.getIp());
        audit.setArea(visitInfo.getArea());
        audit.setCity(visitInfo.getCity());
        // 记录业务数据
        if (auditLog.isSaveRequestData()) {
            audit.setEntityId(auditLog.bizId());
            List<Map> diffRecord = (List<Map>) AuditLogContextHolder.getVariable(AuditLogContextHolder.CONTEXT_KEY_NAME_DIFF_RECORD);
            if (!CollectionUtil.isEmpty(diffRecord)) {
                List<BeanPostLogInfoDTO> details = new ArrayList();
                for (Map record : diffRecord) {
                    BeanPostLogInfoDTO dto = BeanPostLogInfoDTO.builder().build();
                    dto = BeanUtil.mapToBean(record, dto);
                    details.add(dto);
                }
                audit.setInfos(details);
            }
        }
        // 记录系统消息
        String message = auditLog.message();
        if (StringUtils.isNotEmpty(message)) {
            message = Convert.toStr(parseExpress(message, AuditLogContextHolder.getContext()), "");
        }
        audit.setDesc(message);

        if (null != throwable) {
            audit.setStatus(StatusType.FAIL.getCode());
            audit.setDesc(StringUtils.substring(throwable.getMessage(), 0, 2000));
        } else {
            audit.setStatus(StatusType.SUCCESS.getCode());
        }

        logFactory.getBeanPostLog().save(audit);
        AuditLogContextHolder.clearContext();
    }

    private Method getMethod(JoinPoint joinPoint) {
        Method method = null;
        try {
            Signature signature = joinPoint.getSignature();
            MethodSignature ms = (MethodSignature) signature;
            Object target = joinPoint.getTarget();
            method = target.getClass().getMethod(ms.getName(), ms.getParameterTypes());
        } catch (NoSuchMethodException e) {
            log.error("OperationLogAspect getMethod error", e);
        }
        return method;
    }

    private Object parseExpress(String express, StandardEvaluationContext context) {
        try {
            Expression expression = spelExpressionParser.parseExpression(express, PARSER_CONTEXT);
            return expression.getValue(context, Object.class);
        } catch (Exception e) {
            return null;
        }
    }
}
